mod scram;
mod utils;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use hex::{FromHex, encode as to_hex};
use scram::*;
use serde::{Deserialize, Serialize};
use std::{
    fs::{File, OpenOptions},
    io::Write,
    sync::LazyLock,
    thread::sleep,
    time::Duration,
};
use ureq::Agent;

macro_rules! log_if_error {
    ($it: expr) => {
        match $it {
            Ok(r) => Some(r),
            Err(e) => {
                println!("{e:?}");
                None
            }
        }
    };
}

const RAW_PW: &str = include_str!("testdata_rawpw").trim_ascii();
static STATE_FILE: LazyLock<File> = LazyLock::new(|| {
    OpenOptions::new()
        .append(true)
        .create(true)
        .open("main.state")
        .expect("Unable to open state file!")
});

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct DeviceInfo {
    #[serde(rename = "Active")]
    active: bool,
    #[serde(rename = "HostName")]
    hostname: String,
    #[serde(rename = "ActualName")]
    modified_name: String,
    #[serde(rename = "MACAddress")]
    mac_addr: String,
    #[serde(rename = "IPAddress")]
    ip_addr: String,
    #[serde(rename = "AccessRecord")]
    access_record: String,
    #[serde(rename = "OfflineRecord")]
    offline_record: String,
    #[serde(rename = "DeviceMaxDownLoadRate")]
    max_down_rate: u32,
    #[serde(rename = "DeviceDownRateEnable")]
    down_rate_limit_on: bool,
}

impl DeviceInfo {
    fn display(&self) -> Result<()> {
        let last_access = self
            .access_record
            .split('#')
            .next()
            .context("Display devinfo: cannot find '#' in access_record")?;
        let last_depart = self
            .offline_record
            .split('#')
            .next()
            .context("Display devinfo: cannot find '#' in offline_record")?;
        println!("{self:#?}");
        println!(
            "{:?} from last entry, {:?} from last departure",
            duration_since(last_access).context("Display devinfo: last access time parse")?,
            duration_since(last_depart).context("Display devinfo: last depart time parse")?,
        );
        Ok(())
    }
}

#[derive(Parser)]
struct Cli {
    dev_id: String,
    #[arg(short, long)]
    retry_interval_secs: Option<u64>,
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    CheckStep {
        #[arg(short)]
        check_interval_secs: Option<u64>,
        #[arg(short)]
        uptime: Option<u64>,
        #[arg(short)]
        downtime: Option<u64>,
    },
    Info,
    Rlset,
    Rlclear,
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    let dev_id = &cli.dev_id;
    let retry_interval = Duration::from_secs(cli.retry_interval_secs.unwrap_or(2));

    match cli.cmd {
        Cmd::CheckStep {
            check_interval_secs,
            uptime,
            downtime,
        } => check_activated_step(
            Duration::from_secs(check_interval_secs.unwrap_or(2 * 60)),
            retry_interval,
            Duration::from_secs(uptime.unwrap_or(30 * 60)),
            Duration::from_secs(downtime.unwrap_or(30 * 60)),
            dev_id,
        ),
        Cmd::Info => loop {
            if let Some((_, _, devinfo_lst)) = log_if_error!(login_and_get_devinfo_lst()) {
                if let Some(info) = log_if_error!(extract_devinfo(&devinfo_lst, dev_id)) {
                    info.display()?;
                }
                break;
            }
            sleep(retry_interval);
        },
        Cmd::Rlset => {
            while log_if_error!(set_rate_limit(dev_id, true)).is_none() {
                sleep(retry_interval)
            }
        }
        Cmd::Rlclear => {
            while log_if_error!(set_rate_limit(dev_id, false)).is_none() {
                sleep(retry_interval)
            }
        }
    }
    Ok(())
}

fn check_activated_step(
    check_interval: Duration,
    retry_interval: Duration,
    up_time: Duration,
    down_time: Duration,
    dev_id: &str,
) {
    loop {
        'stage1: loop {
            sleep(check_interval);
            loop {
                if let Some(on) = log_if_error!(check_on(dev_id)) {
                    if on {
                        let now = jiff::Zoned::now();
                        let _ = writeln!(&*STATE_FILE, "{now:?}: {on}");
                        break 'stage1;
                    }
                    break;
                }
                sleep(retry_interval);
            }
        }

        sleep(up_time);
        while log_if_error!(set_rate_limit(dev_id, true)).is_none() {
            sleep(retry_interval)
        }

        sleep(down_time);
        while log_if_error!(set_rate_limit(dev_id, false)).is_none() {
            sleep(retry_interval)
        }
    }
}

#[derive(Debug, Serialize)]
struct UserLoginNonceReqData<'a> {
    #[serde(rename = "username")]
    user_name: &'a str,
    #[serde(rename = "firstnonce")]
    first_nonce_hex: &'a str,
}
#[derive(Debug, Serialize)]
struct UserLoginProofReqData<'a> {
    #[serde(rename = "clientproof")]
    client_proof_hex: &'a str,
    #[serde(rename = "finalnonce")]
    final_nonce_hex: &'a str,
}
#[derive(Debug, Serialize)]
struct DataWithCsrf<'a, T> {
    data: T,
    csrf: &'a CsrfInfo,
}

#[derive(Debug, Serialize, Deserialize)]
struct CsrfInfo {
    #[serde(rename = "csrf_param")]
    csrf_param_hex: String,
    #[serde(rename = "csrf_token")]
    csrf_token_hex: String,
}
#[derive(Debug, Deserialize)]
struct UserLoginNonceResponse {
    iterations: usize,
    #[serde(rename = "servernonce")]
    server_nonce_hex: String,
    #[serde(rename = "salt")]
    salt_hex: String,
    #[serde(flatten)]
    csrf: CsrfInfo,
}
/* #[derive(Debug, Deserialize)]
struct UserLoginProofResponse {
    #[serde(rename = "rsan")]
    _rsa_n_hex: String,
    #[serde(rename = "rsae")]
    _rsa_e_hex: String,
    #[serde(rename = "rsapubkeysignature")]
    _rsa_pubkey_signature_hex: String,
    #[serde(rename = "serversignature")]
    _server_signature_hex: String,
    #[serde(flatten)]
    csrf: CsrfExtracted,
} */

fn login_and_get_devinfo_lst() -> Result<(Agent, CsrfInfo, String)> {
    let agent = Agent::new_with_defaults();

    let csrf: CsrfInfo = agent
        .get("http://192.168.3.1/api/system/deviceinfo")
        .call()
        .context("Login: Stp1, call")?
        .into_body()
        .read_json()
        .context("Login: Stp1, read_json")?;

    let first_nonce_hex = to_hex(rand_32_bytes());

    let step_1_res: UserLoginNonceResponse = agent
        .post("http://192.168.3.1/api/system/user_login_nonce")
        .send_json(DataWithCsrf {
            csrf: &csrf,
            data: UserLoginNonceReqData {
                user_name: "admin",
                first_nonce_hex: &first_nonce_hex,
            },
        })
        .context("Login: Stp2, call")?
        .into_body()
        .read_json()
        .context("Login: Stp2, read_json")?;

    let UserLoginNonceResponse {
        iterations,
        server_nonce_hex,
        salt_hex,
        csrf,
    } = step_1_res;

    let salt: [u8; 32] = FromHex::from_hex(salt_hex).unwrap();
    let authmsg = first_nonce_hex + "," + &server_nonce_hex + "," + &server_nonce_hex;

    let salted_password = salted_password(RAW_PW.as_bytes(), &salt, iterations);

    let client_key = {
        let mut it = client_key(&salted_password);
        let stored_key = stored_key(&it);
        let sig = signature(&stored_key, authmsg.as_bytes());
        for (ck_b, sig_b) in it.iter_mut().zip(sig.iter().copied()) {
            *ck_b ^= sig_b
        }
        it
    };

    let client_key_hex = to_hex(client_key);
    let csrf: CsrfInfo = agent
        .post("http://192.168.3.1/api/system/user_login_proof")
        .send_json(DataWithCsrf {
            data: UserLoginProofReqData {
                client_proof_hex: &client_key_hex,
                final_nonce_hex: &server_nonce_hex,
            },
            csrf: &csrf,
        })
        .context("Login: Stp3, call")?
        .into_body()
        .read_json()
        .context("Login: Stp3, read_json")?;

    let devinfo_lst = agent
        .get("http://192.168.3.1/api/system/HostInfo")
        .call()
        .context("Login: Stp4, call")?
        .into_body()
        .read_to_string()
        .context("Login: Stp4, read_to_string")?;
    Ok((agent, csrf, devinfo_lst))
}

fn check_on(dev_id: &str) -> Result<bool> {
    let (_, _, devinfo_lst) = login_and_get_devinfo_lst()?;
    extract_devinfo_check_active(&devinfo_lst, dev_id)
}

fn set_rate_limit(dev_id: &str, on: bool) -> Result<()> {
    let (agent, csrf, devinfo_lst) = login_and_get_devinfo_lst()?;

    let modifired_devinfo = extract_devinfo_set_rate_limit(&devinfo_lst, dev_id, on)?;

    let data = format!(
        r#"{{"csrf":{{"csrf_param":"{cp}","csrf_token":"{ct}"}},"data":{data}}}"#,
        cp = csrf.csrf_param_hex,
        ct = csrf.csrf_token_hex,
        data = modifired_devinfo
    );

    let _csrf: CsrfInfo = agent
        .post("http://192.168.3.1/api/app/qosclass_host")
        .content_type("application/json")
        .send(data.as_bytes())
        .context("send rate limit request")?
        .into_body()
        .read_json()
        .context("read csrf info from the rate limit request response")?;

    Ok(())
}

fn extract_devinfo_raw<'s>(src: &'s str, dev_id: &str) -> Result<&'s str> {
    let dev_id_start = src
        .find(dev_id)
        .context("Extract devinfo: cannot find the dev id")?;
    let dev_info_start = src[..dev_id_start]
        .rfind('{')
        .context("Extract devinfo: cannot find open brace")?;
    let dev_info_end = dev_info_start
        + src[dev_info_start..]
            .find('}')
            .context("Extract devinfo: cannot find close brace")?;

    Ok(&src[dev_info_start..=dev_info_end])
}

fn extract_devinfo(src: &str, dev_id: &str) -> Result<DeviceInfo> {
    serde_json::from_str(extract_devinfo_raw(src, dev_id)?)
        .context("Extract devinfo: cannot turn into json")
}

fn extract_devinfo_check_active(src: &str, dev_id: &str) -> Result<bool> {
    const THE_FIELD: &str = "\"Active\":";

    let info_raw = extract_devinfo_raw(src, dev_id)?;

    let after_the_field_end = info_raw
        .find(THE_FIELD)
        .context("Check active: cannot find the field")?
        + THE_FIELD.len();
    let field_value_end = after_the_field_end
        + info_raw[after_the_field_end..]
            .find(',')
            .context("Check active: cannot find the seperator")?;

    Ok(info_raw[after_the_field_end..field_value_end].trim() == "true")
}

fn extract_devinfo_set_rate_limit(src: &str, dev_id: &str, rate_limit_on: bool) -> Result<String> {
    const THE_FIELD: &str = "\"DeviceDownRateEnable\"";

    let info_raw = extract_devinfo_raw(src, dev_id)?;

    let after_the_field_end = info_raw
        .find(THE_FIELD)
        .context("Set rate limit: cannot find the field")?
        + THE_FIELD.len();
    let field_value_end = after_the_field_end
        + info_raw[after_the_field_end..]
            .find(',')
            .context("Set rate limit: cannot find the seperator")?;
    let before = &info_raw[..after_the_field_end];
    let after = &info_raw[field_value_end..];
    Ok(format!(
        "{before}:{}{after}",
        if rate_limit_on { "true" } else { "false" }
    ))
}

fn duration_since(time_str: &str) -> Result<jiff::Span> {
    use jiff::{SpanRound, Unit, Zoned, civil::DateTime, tz::TimeZone};
    const TZ: TimeZone = TimeZone::fixed(jiff::tz::offset(8));
    const FMT: &str = "%Y-%m-%d %H:%M:%S";

    let parsed = DateTime::strptime(FMT, time_str).context("time parse error")?;
    let now = Zoned::now().with_time_zone(TZ).datetime();
    let round_opt = SpanRound::new().smallest(Unit::Minute);

    Ok((now - parsed).round(round_opt).unwrap())
}
